﻿using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Odbc;
using System.Data.SqlClient;
using System.Linq;
using System.Text;

// Copyright © Microsoft Corporation.  All Rights Reserved.
// This code released under the terms of the 
// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)

namespace agtest
{
	/// <summary>
	/// Parses and encapsulates failover test parameters for agtest.
	/// </summary>
	public class FailoverTestParameters
	{
		/// <summary>Number of required argumnents for parser.</summary>
		private const int RequiredArgs = 4;

		/// <summary>Supported driver types for performance comparison.</summary>
		public enum DriverType { SNAC, SqlClient, SqlServer }

		/// <summary>Milliseconds delay between each new connection attempt.</summary>
		public int ConnectionRetryMs { get; private set; }

		/// <summary>Milliseconds delay after processing an exception.</summary>
		public int ExceptionRetryMs { get; private set; }

		/// <summary>Driver connection timeout.</summary>
		public int ConnectTimeout { get; private set; }

		/// <summary>Number of failover attempts in the test run.</summary>
		public int FailoverLoops { get; private set; }

		/// <summary>Client driver to use.</summary>
		public DriverType DriverToUse { get; private set; }

		/// <summary>Set to true to enable multisubnet failover support in connection string.</summary>
		public bool UseMultiSubnetFailover { get; set; }

		/// <summary>Set to true to enable application intent = readonly in connection string.</summary>
		public bool UseReadOnlyIntent { get; set; }

		/// <summary>Set to true to log statistics to FailoverStatistics database.</summary>
		public bool LogStatistics { get; set; }

		/// <summary>Returns true if database mirroring connectivity algorithm is used.</summary>
		public bool UseDatabaseMirroring
		{
			get
			{
				return !string.IsNullOrEmpty(FailoverPartner);
			}
		}

		/// <summary>Log errors that occur during failover.</summary>
		public bool LogFailoverErrors { get; private set; }

		/// <summary>Custom tag used when writing test data to SQL Server (helpful to classify which test you were running).</summary>
		public string Tag { get; private set; }

		/// <summary>Target server name (typically the AG listener name, but could be normal server for database mirroring test).</summary>
		public string Server { get; private set; }

		/// <summary>Target database for test.</summary>
		public string Database { get; private set; }

		/// <summary>Failover partner for database mirroring failover testing.</summary>
		public string FailoverPartner { get; private set; }

		/// <summary>Logging server for uploading failover stats.</summary>
		public string LogServer { get; private set; }

		/// <summary>Base connection string calculated from input parameters</summary>
		private string BaseConnectionString { get; set; }

		public string ConnectionString 
		{
			get
			{
				StringBuilder cs = new StringBuilder(1000);
				cs.Append(BaseConnectionString);

				if (UseMultiSubnetFailover)
				{
					cs.Append("MultiSubnetFailover=Yes;");
				}
				if (UseReadOnlyIntent)
				{
					cs.Append("ApplicationIntent=ReadOnly;");
				}

				return cs.ToString();
			}
		}

		/// <summary>Current availability group name extracted from server.</summary>
		public string AvailabilityGroupName { get; set; }

		/// <summary>Current availability group id  extracted from server.</summary>
		public Guid AvailabilityGroupId { get; set; }

		/// <summary>
		/// List of nodes in the availability group.
		/// </summary>
		public List<string> Replicas = new List<string>();

		/// <summary>
		/// List of nodes in the availability group for display purposes (comma separated list).
		/// </summary>
		public string ReplicasForDisplay
		{
			get
			{
				StringBuilder sb = new StringBuilder(1000);
				foreach (string node in Replicas)
				{
					sb.Append(node);
					sb.Append(',');
				}
				if (sb.Length > 0)
				{
					sb.Length = sb.Length - 1;
				}
				return sb.ToString();
			}
		}

		/// <summary>Current primary server prior to failover.</summary>
		public string PrimaryServer { get; set; }

		/// <summary>Target primary server after failover.</summary>
		public string FailoverServer { get; set; }

		/// <summary>Number of replicas in availability group.</summary>
		public int ReplicaCount { get; set; }

		/// <summary>
		/// Creates a new connection (unopened) of the specified driver type.
		/// </summary>
		public DbConnection GetNewConnection()
		{
			switch (DriverToUse)
			{
				case DriverType.SNAC:
				case DriverType.SqlServer:
					OdbcConnection connOdbc = new OdbcConnection(ConnectionString);
					if (0 != ConnectTimeout)
					{
						connOdbc.ConnectionTimeout = ConnectTimeout;
					}
					return (DbConnection)connOdbc;
				case DriverType.SqlClient:
					SqlConnection connSqlClient = new SqlConnection(ConnectionString);
					return (DbConnection)connSqlClient;
			}
			return null;
		}

		/// <summary>
		/// Returns new unopened SqlConnection to specified target.
		/// </summary>
		public SqlConnection GetNewSqlConnection()
		{
			SqlConnectionStringBuilder csb = new SqlConnectionStringBuilder();
			csb.DataSource = Server;
			csb.InitialCatalog = Database;
			csb.IntegratedSecurity = true;
			csb.Pooling = false;
			if (!string.IsNullOrEmpty(FailoverPartner))
			{
				csb.FailoverPartner = FailoverPartner;
			}
			if (0 != ConnectTimeout)
			{
				csb.ConnectTimeout = ConnectTimeout;
			}
			SqlConnection conn = new SqlConnection(csb.ConnectionString);
			return conn;
		}

		private static void DisplayUsage()
		{
			Console.WriteLine("usage:");
			Console.WriteLine("agtest <tag> <server> <database> <loops> [-sqlclient|-snac|-sqlserver] [-msf] [-ro]");
			Console.WriteLine("         [-fp <failover partner>] [-logserver <server>]");
			Console.WriteLine();
			Console.WriteLine("  <tag>           => Tag for prefix on log file (required) as well as test results table");
			Console.WriteLine("  <server>        => The target SQL Server server (required)");
			Console.WriteLine("  <database>      => The target database (required)");
			Console.WriteLine("  <loops>         => Number of failover loops (required)");
			Console.WriteLine("  -sqlclient      => Use SqlClient driver");
			Console.WriteLine("  -snac           => Use SNAC 11 ODBC driver (default if not specified)");
			Console.WriteLine("  -sqlserver      => Use SqlServer (MDAC) ODBC driver");
			Console.WriteLine("  -msf            => Use MultiSubnetFailover=True");
			Console.WriteLine("  -ro             => Use ApplicationIntent=ReadOnly");
			Console.WriteLine("  -fp <failover>  => Set failover partner for database mirroring failover test.");
			Console.WriteLine("  -logserver <server> => Specify a SQL Server for uploading failover statistics.");

			Console.WriteLine();
			Console.WriteLine("Examples:");
			Console.WriteLine("To test 10 failovers for availability group listener AG1VNN with database DB1 with ADO.NET driver =>");
			Console.WriteLine("   agtest TestAdoFailover1 AG1VNN DB1 10 -sqlclient -msf");
			Console.WriteLine("To test 5 failovers for availability group listener AG2VNN with database DB2 with SNAC driver =>");
			Console.WriteLine("   agtest TestSnacFailover1 AG1VNN DB2 5 -snac -msf");
			Console.WriteLine("To test 5 failovers using database mirroring failover method between Node1 and Node2 servers using database DB3 and SNAC driver =>");
			Console.WriteLine("   agtest TestMirroringFailover1 Node1 DB3 5 -snac -fp Node2");
		}

		/// <summary>
		/// Parses command line parameters and returns CommandLineParameters struct.
		/// </summary>
		/// <param name="args">Args array from main</param>
		public static FailoverTestParameters ParseCommandLine(string[] args)
		{
			// Display usage for no args or -? /?.
			if (0 == args.Length || args.Any(arg => arg.Contains("-?") | arg.Contains("/?")))
			{
				DisplayUsage();
				return null;
			}

			// Check minimum requirement.
			if (args.Length < RequiredArgs)
			{
				Console.WriteLine("Tag + server + database + loops are required arguments.");
				DisplayUsage();
				return null;
			}

			FailoverTestParameters ftp = new FailoverTestParameters();

			// Load required parameters.
			ftp.Tag = args[0];
			ftp.Server = args[1];
			ftp.Database = args[2];
			int failLoops = 0;
			if (!int.TryParse(args[3], out failLoops))
			{
				Console.WriteLine("Error parsing loops parameter {0}, you must specify an integer value.", args[3]);
				DisplayUsage();
				return null;
			}
			ftp.FailoverLoops = failLoops;

			// Set defaults.
			ftp.LogFailoverErrors = true;
			ftp.ConnectionRetryMs = 1000;
			ftp.ExceptionRetryMs = 1000;
			ftp.ConnectTimeout = 0;

			// Parse rest of parameters.
			for (int i = RequiredArgs; i < args.Length; i++)
			{
				string nextArgString = args[i].ToLower();
				char nextArgChar = (nextArgString[0] == '-') ? nextArgString[1] : 'x';
				try
				{
					switch (nextArgChar)
					{
						case 'f':
							if (i + 1 >= args.Length)
							{
								Console.WriteLine("{0} missing failover partner", nextArgString);
								DisplayUsage();
								return null;
							}
							ftp.FailoverPartner = args[i+1];
							i++;
							break;
						case 'l':
							if (i + 1 >= args.Length)
							{
								Console.WriteLine("{0} missing log server", nextArgString);
								DisplayUsage();
								return null;
							}
							ftp.LogServer = args[i+1];
							i++;
							break;
						case 'm':
							ftp.UseMultiSubnetFailover = true;
							break;
						case 'r':
							ftp.UseReadOnlyIntent = true;
							break;
						case 's':
							if (nextArgString.Equals("-snac"))
							{
								ftp.DriverToUse = FailoverTestParameters.DriverType.SNAC;
							}
							else
							if (nextArgString.Equals("-sqlclient"))
							{
								ftp.DriverToUse = FailoverTestParameters.DriverType.SqlClient;
							}
							else
							if (nextArgString.Equals("-sqlserver"))
							{
								ftp.DriverToUse = FailoverTestParameters.DriverType.SqlServer;
							}
							else
							{
								Console.WriteLine("The argument '{0}' is not recognized, expected -snac, -sqlclient or -sqlserver.", nextArgString);
								DisplayUsage();
								return null;
							}
							break;
						case 'x':
							Console.WriteLine("The argument '{0}' specified is not a recognized argument.", nextArgString);
							DisplayUsage();
							return null;
					}
				}
				catch (Exception ex)
				{
					Console.WriteLine("Exception parsing argument # {0} ({1})", i, nextArgChar);
					Console.WriteLine(ex.Message);
					return null;
				}
			}

			// Calculate connection string for individual driver.
			StringBuilder cs = new StringBuilder(1000);
			switch (ftp.DriverToUse)
			{
				case DriverType.SNAC:
					cs.Append(string.Format("Driver=SQL Server Native Client 11.0;Server={0};Database={1};Trusted_Connection=Yes;", ftp.Server, ftp.Database));
					if (!string.IsNullOrEmpty(ftp.FailoverPartner))
					{
						cs.Append(string.Format("Failover_Partner={0};", ftp.FailoverPartner));
					}
					break;
				case DriverType.SqlClient:
					cs.Append(string.Format("Server={0};Database={1};Integrated Security=SSPI;Pooling=False;", ftp.Server, ftp.Database));
					if (!string.IsNullOrEmpty(ftp.FailoverPartner))
					{
						cs.Append(string.Format("Failover Partner={0};", ftp.FailoverPartner));
					}
					if (0 != ftp.ConnectTimeout)
					{
						cs.Append(string.Format("Connect Timeout={0};", ftp.ConnectTimeout));
					}
					break;
				case DriverType.SqlServer:
					cs.Append(string.Format("Driver=SQL Server;Server={0};Database={1};Trusted_Connection=Yes;", ftp.Server, ftp.Database));
					if (!string.IsNullOrEmpty(ftp.FailoverPartner))
					{
						Console.WriteLine("-fp (failover partner) not supported by the -sqlserver driver.");
						return null;
					}
					if (ftp.UseMultiSubnetFailover)
					{
						Console.WriteLine("-msf (Multisubnetfailover) not supported by the -sqlserver driver.");
						return null;
					}
					if (ftp.UseReadOnlyIntent)
					{
						Console.WriteLine("-ro (Read-only intent) not supported by the -sqlserver driver.");
						return null;
					}
					break;
			}
			ftp.BaseConnectionString = cs.ToString();

			return ftp;
		}
	}
}
